<template>
  <div>
    <!-- 不是空闲状态不能选择文件 -->
    <input
      type="file"
      :disabled="status !== Status.wait"
      @change="handleFileChange"
    />
    <el-button type="primary" @click="handleUpload" :disabled="uploadDisabled()">上传</el-button>

    <!-- 不是暂停状态不显示恢复按钮 -->
    <el-button v-if="status === Status.pause" @click="handleResume">恢复</el-button>
    <!--
      1.当已经是暂停状态时 不显示该按钮
      2.当文件不是在上传过程中不能点暂停
      3.当文件hash没算出来是不能点暂停 
    -->
    <el-button
      v-else
      :disabled="status !== Status.uploading || status==Status.merging ||!container.hash"
      @click="handlePause"
      >暂停</el-button>

    <h4>文件hash完成情况：</h4>
    <el-progress
      :percentage="hashPercentage"
      :text-inside="true"
      :stroke-width="18"
    ></el-progress>

    <h3>总进度</h3>
    <!-- 每个blob 进度 计算出来？ 
      1. 每块blob 上传  值percentage 变的， watch 
      2. 计算属性 computed -->
    <el-progress
      :text-inside="true"
      :percentage="totalUploadPercentage"
      :stroke-width="26"
      color="#43b244"
    ></el-progress>

    <!-- 多个切片 -->
    <!-- <el-table :data="data">
      <el-table-column prop="hash" label="切片hash"> </el-table-column>
      <el-table-column label="大小(MB)">
        <template v-slot="{ row }">
          {{ row.size | transformToMB }}
        </template>
      </el-table-column>
      <el-table-column label="进度">
        <template v-slot="{ row }">
          <el-progress :percentage="row.percentage" color="#909399">
          </el-progress>
        </template>
      </el-table-column>
    </el-table> -->
    <hr />
    <div class="cube-container" :style="{ width: cubeWidth + 'px' }">
      <div class="cube" v-for="chunk in data" :key="chunk.hash">
        <div
          :class="{
            'cube-uploading': chunk.percentage > 0 && chunk.percentage < 100,
            'cube-success': chunk.percentage == 100,
            'cube-error': chunk.percentage < 0,
          }"
          :style="{ height: chunk.percentage + '%' }"
        >
          <i
            v-if="chunk.percentage > 0 && chunk.percentage < 100"
            class="el-icon-loading"
            style="color: #f56c6c"
          ></i>
        </div>
      </div>
    </div>
    <el-button v-if="status===Status.merging " type="warning" :loading="true">文件合并中</el-button>
  </div>
</template>
<!--
<style lang="stylus" scoped>
.cube-container
  width 100px
  overflow hidden
.cube
  width 14px
  height 14px
  line-height 12px;
  border 1px solid black
  background  #eee
  float left
  >.success
    background #67C23A
  >.uploading
    background #409EFF
  >.error
    background #F56C6C


</style>
-->
<style scoped>
.cube-container {
  width: 100px;
  overflow: hidden;
}
.cube {
  width: 14px;
  height: 14px;
  line-height: 12px;
  border: 1px solid black;
  background: #eee;
  float: left;
}
.cube-success {
  background: #67c23a;
}
.cube-uploading {
  background: #409eff;
}
.cube-error {
  background: #f56c6c;
}
</style>

<script>
import fileService from "@/api/file";
import SparkMD5 from "../../static/spark-md5.min.js";

//计时器
const moment = require("moment"); //moment.js库
const Timer = {
  data: {},
  start: function (key) {
    Timer.data[key] = new moment();
  },
  stop: function (key) {
    let time = Timer.data[key];
    if (time) {
      Timer.data[key] = new moment() - time;
    }
  },
  getTime: function (key) {
    // console.log('typeof Timer.data[key] ',typeof Timer.data[key]);
    return Timer.data[key].format("YYYY-MM-DD HH:mm:ss"); //.format('yyyy-MM-dd HH:mm:ss')
  },
  getTimeFormatSecond: function (key) {
    return Number.parseFloat(Timer.data[key] / 1000).toFixed(1);
  },
};

// 这个值应该需要前端动态调整
const SIZE = 5 * 1024 * 1024;

const Status = {
  //良好的代码风格 enum  有利于代码的可读性
  wait: "wait",
  pause: "pause",
  uploading: "uploading",
  error: "error",
  done: "done",
  errorLimit: "errorLimit",
  merging: "merging",
};

export default {
  filters: {
    transformToMB(value) {
      //(val/1024).toFixed(0)
      return Number.parseInt(value / 1024 / 1024);
    },
  },
  watch: {
    //监听器：一但 totalUploadPercentageCalculate 发生变化，就会触发方法
    totalUploadPercentageCalculate: function (newValue, oldValue) {
      if (newValue > this.totalUploadPercentage) {
        this.totalUploadPercentage = newValue;
      }
    },
  },
  computed: {
    cubeWidth() {
      return Math.ceil(Math.sqrt(this.data.length)) * 16;
    },

    //总进度计算
    totalUploadPercentageCalculate: function () {
      if (!this.container.file || !this.data.length) return 0;

      const loaded = this.data
        .map(
          item => item.size * item.percentage //每个blob的已上传大下
        )
        .reduce(
          (preValue, curValue) => preValue + curValue //已上传的总文件大小
        );

      // 对数组中的所有元素调用指定的回调函数。function callbackfn(previousValue, currentValue, currentIndex, array1)
      // 该回调函数的返回值为累积结果，并且此返回值在下一次调用该回调函数时作为参数提供。
      return parseInt((loaded / this.container.file.size).toFixed(2));
    },
  },
  data: () => ({
    SIZE, // 分块大小
    Status, // 定义状态，方便引用
    container: {
      file: null,
      hash: "",
    },
    status: Status.wait,
    hashPercentage: 0, // 对整个文件进行 hash 的进度
    totalUploadPercentage: 0, //总的上传进度，通过计算属性得出
    data: [], // 要上传的数据 after shouldUpload
    requestList: [], // 保存着正在请求的 ajax 对象
    fileChunkList: [] //文件块数据的列表
  }),
  methods: {
    showTimeCost() {
      this.$alert(
        `
      <strong>开始时间：</strong>${Timer.getTime("beginTime")};<br />
      <strong>结束时间：</strong>${Timer.getTime("endTime")};<br />
      <strong>文件Hash耗时：</strong>${Timer.getTimeFormatSecond(
        "hashTime"
      )}<br />
      <strong>上传耗时：</strong>${Timer.getTimeFormatSecond(
        "uploadTime"
      )}<br />
      <strong>合并块的时间：</strong>${Timer.getTimeFormatSecond(
        "combineChunk"
      )}<br />
      <strong>总耗时：</strong>${Timer.getTimeFormatSecond("totalTime")}
      `,
        "耗时情况(单位：秒)",
        { dangerouslyUseHTMLString: true }
      ); //,{dangerouslyUseHTMLString: true} 当作 HTML 片段处理
    },
    // setChunkSIZE(fileSize) {
    //   if (fileSize <= 300 * 1024 * 1024) {
    //     this.SIZE = 10 * 1024 * 1024;
    //   } else {
    //     this.SIZE = 20 * 1024 * 1024;
    //   }
    // },
    handlePause() {
      //处理暂停
      this.status = Status.pause;
      // 调用保存在 requestList 中 xhr 的 abort 方法，即取消并清空所有正在上传的切片
      this.requestList.forEach(xhr => xhr.abort());
      this.requestList = [];
    },
    resetData() {
      /*
      $data 指向是组件运行时的 data，这个data 随着用户操作会发生变化
      $options.data 指向的是组件实例化是穿的 function 也就是 data: () => ({ ... }), 
        调用后会返回初始化时的数据， 
      Object.assign 这个就不用说了吧，所以才说是用来还原 data 的
      */
      Object.assign(this.$data, this.$options.data());
    },
    async handleResume() {
      //处理恢复
      this.status = Status.uploading;
      const { uploadedList } = await this.verifyUpload(
        this.container.file.name,
        this.container.hash
      );
      //todo 再过滤一遍， bug: 出现上传成功，但是某些块的percentage的值还是不到100
      //solve: 现在把过滤代码放在uploadChunks() 方法里，每次上传块前都先过滤 ， 把 this.fileChunkList 全局
      await this.uploadChunks(uploadedList);
    },

    uploadDisabled() {
      //文件还没选择是不能点击上传;暂停状态或者上传中状态不能点击上传
      return !this.container.file || this.status !== Status.wait;
      // return (
      //   !this.container.file ||
      //   [Status.pause, Status.uploading].includes(this.status)
      // );
    },

    handleFileChange(e) {
      const [file] = e.target.files; //拿到第一个文件
      // console.log(file);
      if (!file) return;
      Object.assign(this.$data, this.$options.data());
      this.container.file = file;
    },
    async handleUpload(e) {
      Timer.start("beginTime");
      Timer.start("totalTime");
      Timer.start("hashTime");

      if (!this.container.file) return;
      this.status = Status.uploading;

      // this.setChunkSIZE(this.container.file.size);
      console.log(`分片大小： ${this.SIZE / 1024 / 1024}MB`);
      this.$notify({
        type: "info",
        title: "分片大小",
        message: `分片大小： ${this.SIZE / 1024 / 1024}MB`,
        duration: 0,
      });
      this.fileChunkList = this.createFileChunk(this.container.file);
      console.log("fileChunkList", this.fileChunkList);
      console.time("samplehash");
      //  this.container.hash = await this.calculateHash(fileChunkList);
      // 这样抽样，大概1个G 1秒，如果还嫌慢，可以考虑分片+web-worker的方式
      // 这种方式偶尔会误判 不过大题效率不错
      // 可以考虑和全部的hash配合，因为samplehash不存在，就一定不存在，存在才有可能误判，有点像布隆过滤器
      this.container.hash = await this.calculateHashSample();
      console.timeEnd("samplehash");

      Timer.stop("hashTime");

      Timer.start("uploadTime");
      // 判断文件是否已经上传过（hash值是否一样）
      const { shouldUpload, uploadedList } = await this.verifyUpload(
        this.container.file.name,
        this.container.hash
      ); //通过访问后台确认
      console.log("shouldUpload, uploadedList", shouldUpload, uploadedList);

      if (!shouldUpload) {
        //上传过了,秒传
        this.$notify({
          type: "success",
          title: "上传情况",
          message: "秒传，上传成功",
          duration: 0,
        });
        this.status = Status.wait;
        return;
      }
      
      await this.uploadChunks(uploadedList); //上传切片

      Timer.stop("totalTime");
      Timer.start("endTime");
      this.showTimeCost();
    },

    async uploadChunks(uploadedList = []) {

      // 还没上传 或者 没传完
      this.data = this.fileChunkList.map(({ file }, index) => {
        // 每个blob 和 index
        // console.log('file',file);
        // console.log('index',index);
        return {
          fileHash: this.container.hash,
          index,
          hash: this.container.hash + "-" + index, //每个块都有自己的index在内的hash，可排序，可追踪
          chunk: file, //blob 二进制数据
          size: file.size,
          percentage: uploadedList.includes(this.container.hash + "-" + index) ? 100: 0, //当前切片是否已经上传完成
        };
      });

      console.log("uploadChunks() ==> this.data", this.data);
      // 数组数据this.data => 请求数组 =>并发
      const requestList = this.data
        // .filter(({ hash }) => !uploadedList.includes(hash))或 过滤器过滤回调函数中返回值为真的元素
        .filter(({ hash }) => uploadedList.indexOf(hash) == -1)
        .map(({ chunk, hash, index }, i) => {
          // console.log(chunk,hash,index);
          const formData = new FormData();
          formData.append("chunk", chunk);
          formData.append("hash", hash);
          formData.append("filename", this.container.file.name);
          formData.append("fileHash", this.container.hash); //文件hash
          return { formData, index, status: Status.wait }; //每个分块添加状态信息
        });
      //控制并发请求数来发送请求
      await this.sendRequest(requestList);

      // .map(async ({ formData, index }) => {
      //   await request({
      //     url: `/upload`,
      //     data: formData,
      //     onProgress: this.createProgressHandler(this.data[index]), // this.data 要上传的数据 after shouldUpload
      //     requestList: this.requestList, //
      //   });
      // });

      /*
      1. Promise.all()方法返回一个Promise实例，
      如果参数值promise有一个失败，此实例回调失败，失败原因是第一个失败promise的结果
      2. Promise.allSettled()方法返回一个promise,
      该promise在所有给定的promise已被解析或被拒绝后的解析，
      并且每个对象都描述每个promise的结果
      */

      //改进：如果并发请求过多会把浏览器压垮
      //await Promise.allSettled(requestList); //并发发送请求，处理分片文件，将其移动到指定目录下
      //上面完成后就可以发送合并请求了
      Timer.stop("uploadTime");
      // console.log(this.data);

      // await this.mergeRequest();

      Timer.start("combineChunk");
      // 之前上传的切片数量+本次上传的切片数量=所有切片数量
      // todo  应该判断 this.data 的 percentage都为100%
      //solve: 改为 this.data 的 percentage都为100% 的数量判断
      const chunkUploadFinish = this.data.map(item=>{
        item.percentage===100
      })
      console.log("chunkUploadFinish",chunkUploadFinish.length);
      console.log("this.data.length",this.data.length);
      if (chunkUploadFinish.length === this.data.length ) {
        this.$notify({
          type: "info",
          title: "合并提示",
          message: "发送合并请求，开始在后端合并文件",
          duration: 0,
        });

        const data = await this.mergeRequest();
        //const { code, message } = JSON.parse(data);
        // console.log("code,message", code, message);
        this.$notify({
          type: "success",
          title: "合并提示",
          message: "合并完成,文件上传成功，可以上传新的文件了~~ 10秒后重置表单",
          duration: 0,
        });
      }
      /* else {
        // console.log(`有${error}个块上传失败`);
        this.$confirm(
          `共有${
            success + error
          }个块上传,其中成功有${success}个,有${error}个块上传失败,点击确定开始重传`,
          "上传信息",
          {
            type: "error",
            confirmButtonText: "开始重传",
            cancelButtonText: "放弃重传",
          }
        )
          .then(() => {
            this.handleResume();
          })
          .catch(() => {
            console.log("放弃重传了");
          });
      }
      */
      Timer.stop("combineChunk");
    },
    //max来管理请求的并发数
    async sendRequest(urls, max = 3, retryLimit = 3) {
      // urls: [{ formData, index, status: Status.wait },...]
      console.log("urls", urls);
      return new Promise((resolve, reject) => {
        const len = urls.length; //共有多少个请求需要发
        // let idx = 0;
        let counter = 0;
        const retryArr = [];

        const start = async () => {
          console.log("counter,len,max", counter, len, max);
          //有请求，有通道
          while (counter < len && max > 0) {
            max--; //占用通道
            console.log(`No.${counter} start request`);
            // const formData = urls[idx].formData;
            // const index = urls[idx].index;
            // idx++;
            //任务不能仅仅累加获取，而是要根据状态
            // wait和error的可以发送请求，方便重试
            const i = urls.findIndex(
              (v) => v.status == Status.wait || v.status == Status.error
            );
            // console.log("i", i);
            if (i !== -1) {// 还没上传或上传失败的块
              /*
              //成功的就成功了；失败的也已经尝试过3次了
              let success = 0, error = 0;
              urls.forEach((item) => {
                item.status == "done" ? success++ : error++;
              });
              console.log("i==-1 urls ,success,error", urls, success, error);
              return resolve({ success, error });
              */
              urls[i].status = Status.uploading;
              const formData = urls[i].formData;
              const index = urls[i].index;

              if (typeof retryArr[index] == "number") {
                console.log(`No.${index}块 开始重试`);
              }
              // 封装请求并发发送 
              console.log("sendRequest this.requestList",this.requestList);

              fileService.upload(formData,
                  this.createProgressHandler(this.data[index]),
                  this.requestList)
                .then(() => {
                  urls[i].status = Status.done;
                  counter++;
                  //释放通道
                  max++;
                  // urls[counter].done = true; // ?
                  if (counter === len) {
                    this.$notify({
                      type: "success",
                      title: "上传提示",
                      message: "所有块上传成功",
                      duration: 0,
                    });
                    resolve();
                    /*
                    //所有请求都发完了
                    let success = 0,
                      error = 0;
                    urls.forEach((item) => {
                      item.status == "done" ? success++ : error++;
                    });
                    console.log("counter === len urls ,success,error", urls, success, error);
                    return resolve({ success, error });
                    */
                  } else {
                    start();
                  }
                })
                .catch(() => {
                  urls[i].status = Status.error;
                  this.data[index].percentage = -1; //报错的进度条
                  if (typeof retryArr[index] !== "number") {
                    //第一次出错
                    retryArr[index] = 0; //初始化
                  }
                  //重试次数累加
                  retryArr[index]++;
                  //判断重试次数是否超过0次数限制
                  if (retryArr[index] > 3) {
                    this.$notify({
                      type: "error",
                      title: "上传提示",
                      message: `<h4>No.${index}块重试次数超过${retryLimit}次，此次上传失败,请网络恢复后再次上传！<br/>或点击'恢复'继续上传</h4>`,
                      dangerouslyUseHTMLString: true,
                      duration: 0,
                    });
                    this.handlePause();
                    
                    return reject(); // abort所有别的
                  }
                  console.log(`No.${index}块 ${retryArr[index]}次报错`);
                  // 报错次数还没超过限制的，重启
                  max++; //释放通道，但是counter不累加
                  start();
                });
            } // end if
          }
        };
        console.log("进入了这里 start()");
        start();
      });
    },
    async mergeRequest() {
      this.status = Status.merging
      // const result = await  request({
      //   url: "/merge",
      //   headers: {
      //     "content-type": "application/json",
      //   },
        // data: JSON.stringify({
        //   size: SIZE,
        //   fileHash: this.container.hash, // 文件的hash
        //   filename: this.container.file.name,
        // }),
      // });
      const result = await fileService.merge(JSON.stringify({
          size: SIZE,
          fileHash: this.container.hash, // 文件的hash
          filename: this.container.file.name,
        }))
      this.status = Status.wait; //设置wait状态，然后可以重置页面数据

      setTimeout(() => {
        this.resetData();
      }, 10 * 1000);
      return result.data;
    },
    createProgressHandler(item) {
      //创建上传时的处理器
      //返回一个函数
      return (e) => {
        //当前切片是否已经上传完成
        item.percentage = parseInt(String((e.loaded / e.total) * 100));
        //e.loaded,e.total 这两个时上传过程中xhr的属性
        // console.log("e.loaded,e.total", e.loaded, e.total);
      };
    },
    createFileChunk(file, size = SIZE) {
      const fileChunkList = [];
      let cur = 0;
      while (cur < file.size) {
        fileChunkList.push({
          file: file.slice(cur, cur + size),
        });
        cur += size;
      }
      return fileChunkList;
    },
    async verifyUpload(filename, fileHash) {
      // {}  用来解构对象
      // const { data } = await request({
      //   url: "/verify",
      //   headers: {
      //     "content-type": "application/json",
      //   },
        // data: JSON.stringify({
        //   filename,
        //   fileHash,
        // }),
      // });
      const { data } = await fileService.verify(JSON.stringify({
          filename,
          fileHash
        }))
      // console.log("data", data);
      return JSON.parse(data);
    },
    async calculateHash(fileChunkList) {
      return new Promise((resolve) => {
        // js 单线程的 UI 主线程
        // html5 web workers 单独开一个线程 独立于 worker
        // 回调 不会影响原来的UI
        // html5 带来的优化
        // js文件只能放到static文件夹中
        // 1、assets文件夹与static文件夹的区别
        // 区别一：assets文件是src下的，所以最后运行时需要进行打包，而static文件不需要打包就直接放在最终的文件中了
        // 区别二：assets中的文件在vue中的template/style下用../这种相对路径的形式进行引用，在script下必须用@import的方式引入而static下的文件在.vue中的任何地方都只要使用../这种相对路径的方式引入，
        this.container.worker = new Worker("../../static/hash.js");
        this.container.worker.postMessage({ fileChunkList }); //与线程通信的方法 发送
        this.container.worker.onmessage = (e) => {
          //与线程通信，接受
          // console.log(e.data);
          const { percentage, hash } = e.data;
          // console.log(percentage+"------"+hash);
          this.hashPercentage = parseInt(percentage.toFixed(2));
          if (hash) {
            // 计算hash 完成后返回计算好的hash(MD5)值
            resolve(hash);
          }
        };
      });
    },
    // 类布隆hash，计算文件的hash
    async calculateHashSample() {
      return new Promise((resolve) => {
        const spark = new SparkMD5.ArrayBuffer();
        const reader = new FileReader();
        const file = this.container.file;
        //文件大小
        const fileSize = this.container.file.size;

        let offset = 2 * 1024 * 1024;

        // 头2M
        //封装成数组对象
        let fileChunkList = [{file:file.slice(0, offset)}];

        let cur = offset;
        while (cur < fileSize) {
          //最后一块全部加进来
          if (cur + offset >= fileSize) {
            fileChunkList.push({file:file.slice(cur, cur + offset)});
          } else {// 中间的块分别只取前中后的两个字节
            const mid = cur + offset / 2;
            const end = cur + offset;
            fileChunkList.push({file:file.slice(cur, cur + 2)});
            fileChunkList.push({file:file.slice(mid, mid + 2)});
            fileChunkList.push({file:file.slice(end - 2, end)});
          }
          cur += offset;
        }
        console.log("影分身hash fileChunkList", fileChunkList);
        this.container.worker = new Worker("../../static/hash.js");
        this.container.worker.postMessage({ fileChunkList }); //与线程通信的方法 发送
        this.container.worker.onmessage = (e) => {
          //与线程通信，接受
          // console.log(e.data);
          const { percentage, hash } = e.data;
          // console.log(percentage+"------"+hash);
          this.hashPercentage = parseInt(percentage.toFixed(2));
          if (hash) {
            // 计算hash 完成后返回计算好的hash(MD5)值
            resolve(hash);
          }
        };
         
        // //拼接
        // reader.readAsArrayBuffer(new Blob(chunks));
        // console.log("影分身reader", reader);

        // reader.onload = (e) => {
        //   spark.append(e.target.result);
        //   resolve(spark.end());
        // };
      });
    },
  },
};
</script>

